#SORACOM EndorseでgRPCアプリのSIM認証を実装する
ども、大瀧です。この記事はSORACOM リリース 1周年記念リレーブログの3日目です。
Google製RPCフレームワークgRPCのIoT向け活用として、#SORACOM BeamでgRPCアプリのトラフィックをTLS暗号化するをご紹介しました。今回はSORACOMの認証トークン発行サービスであるSORACOM EndorseをgRPCのクライアント認証に利用する構成をご紹介します。
SORACOM EndorseのトークンとgRPCのクライアント認証
SORACOM EndorseはSORACOM Air専用のトークン発行サービスです。SIMごとに一意なJWT形式のトークンを簡単なHTTPSリクエストで取得できるため、手軽にSIM単位のクライアント認証を実装することができます。利用例として以下があります。
gRPCには組み込みでGoogle OAuth2のクライアント認証がありますが、今回はシンプルにgRPCのMetadataとしてJWTトークンをリクエストに含め、gRPCサーバーでそれを検証する形にしてみました。
gRPCクライアント、サーバーはGo版のhelloworldサンプルを利用しました。
gRPCクライアントの実装
gRPCクライアントではSORACOM Air SIMによる通信を前提に、EndorseへのHTTPSリクエストを送出、レスポンスをgRPCメタデータにセットします。まずは依存ライブラリをimport
に追加しました。
import ( : "encoding/json" "io/ioutil" "net/http" : "google.golang.org/grpc/metadata" : )
EndorseへのHTTPSリクエストの送出、レスポンスを受け取る処理はgetEndorseJWT関数を定義しました。レスポンスのJSONに対応する構造体EndorseBodyも合わせて定義します。
type EndorseBody struct { Token string `json:"token"` } func getEndorseJWT() string { resp, _ := http.Get("https://endorse.soracom.io/") defer resp.Body.Close() bodyByteArray, _ := ioutil.ReadAll(resp.Body) var eb EndorseBody json.Unmarshal(bodyByteArray, &eb) return eb.Token }
main関数では、先ほど定義したgetEndorseJWT関数を呼び出してJWTトークンを取得しメタデータ"Authirozation"
にセットします(10〜14行目)。セットしたメタデータを付与する形でgRPCサーバーへのリクエストメソッドを実行します(21行目)。
func main() { // Set up a connection to the server. conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn) token := getEndorseJWT() log.Printf("JWTToken: %s", token) md := metadata.Pairs("Authorization", token) ctx := context.Background() ctx = metadata.NewContext(ctx, md) // Contact the server and print out its response. name := defaultName if len(os.Args) > 1 { name = os.Args[1] } r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) //r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.Message) }
ざっくりな作りなので、本来はEndorseへのリクエスト送出のエラーチェック(通信エラーとSORACOM Air以外からの通信についての例外処理)を実装するべきです。
gRPCサーバーの実装
gRPCサーバーでは、依存ライブラリとしてクライアントと同じmetadata
と、検証するJWTのライブラリをインポートします。
import ( : "google.golang.org/grpc/metadata" : "github.com/dgrijalva/jwt-go" : )
JWTトークンのうちSIMに関する要素を含む構造体を宣言します。
type jwtCustomClaims struct { Endorse soracomEndorseClaims `json:"soracom-endorse-claim"` jwt.StandardClaims } type soracomEndorseClaims struct { Imsi string `json:"imsi"` Imei string `json:"imei"` }
gRPCクライアントのリクエストに対応するSayHello関数では、第1引数に含まれるメタデータからJWTトークンを取り出します(3,4行目)。続いてJWTトークンを検証()に含まれる検証用URLを取り出し(5〜10行目)、トークンに含まれる公開鍵での検証を行うlookupPublicKey関数を呼び出し、公開鍵による検証を実行します。問題がなければ、トークンに含まれるIMSIをログに表示します。(11〜14行目)
SORACOM Endorse で発行されたトークンを検証するを参考に、
// SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { md, _ := metadata.FromContext(ctx) tokenString := md["authorization"][0] token, err := jwt.ParseWithClaims(tokenString, &jwtCustomClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return lookupPublicKey(token.Header["kid"].(string)) }) if err == nil && token.Valid { claims := token.Claims.(*jwtCustomClaims) fmt.Printf("JWT validation: OK / IMSI: %s", claims.Endorse.Imsi) } return &pb.HelloReply{Message: "Hello " + in.Name}, nil } func lookupPublicKey(kid string) (*rsa.PublicKey, error) { resp, _ := http.Get("https://s3-ap-northeast-1.amazonaws.com/soracom-public-keys/" + kid) defer resp.Body.Close() bodyByteArray, _ := ioutil.ReadAll(resp.Body) parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(bodyByteArray) return parsedKey, err }
サーバー側のログ(標準出力)は以下のような感じです。
JWT validation: OK / IMSI: 4401XXXXXXXXXXX JWT validation: OK / IMSI: 4401XXXXXXXXXXX JWT validation: OK / IMSI: 4401XXXXXXXXXXX
取得したIMSIを別途リストと照会させてIMSI縛りの認証としても良いですし、ログに残して証跡として利用するのも良いと思います。
まとめ
SORACOM Endorseの発行するJWTトークンを利用したgRPCアプリの認証をご紹介しました。IoT向けだけでなくモバイル向けなどでも応用できる仕組みではないでしょうか。 また、gRPCのメタデータはリクエストと程よく分離されていて、今回のような認証の仕組みを自分で追加するのに便利だなと思いました。